// Copyright (c) 2022. Eritque arcus and contributors.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or any later version(in your opinion).
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

#pragma once

#include <cstddef>   // nullptr_t
#include <exception> // exception
#include <stdexcept> // runtime_error
#include <string>    // to_string
#include <vector>    // vector

#include <nlohmann/detail/input/position_t.hpp>
#include <nlohmann/detail/macro_scope.hpp>
#include <nlohmann/detail/meta/cpp_future.hpp>
#include <nlohmann/detail/meta/type_traits.hpp>
#include <nlohmann/detail/string_concat.hpp>
#include <nlohmann/detail/string_escape.hpp>
#include <nlohmann/detail/value_t.hpp>


NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail {

    ////////////////
    // exceptions //
    ////////////////

    /// @brief general exception of the @ref basic_json class
    /// @sa https://json.nlohmann.me/api/basic_json/exception/
    class exception : public std::exception {
    public:
        /// returns the explanatory string
        const char *what() const noexcept override {
            return m.what();
        }

        /// the id of the exception
        const int id; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)

    protected:
        JSON_HEDLEY_NON_NULL(3)
        exception(int id_, const char *what_arg) : id(id_), m(what_arg) {} // NOLINT(bugprone-throw-keyword-missing)

        static std::string name(const std::string &ename, int id_) {
            return concat("[json.exception.", ename, '.', std::to_string(id_), "] ");
        }

        static std::string diagnostics(std::nullptr_t /*leaf_element*/) {
            return "";
        }

        template<typename BasicJsonType>
        static std::string diagnostics(const BasicJsonType *leaf_element) {
#if JSON_DIAGNOSTICS
            std::vector<std::string> tokens;
            for (const auto *current = leaf_element; current != nullptr && current->m_parent != nullptr; current = current->m_parent) {
                switch (current->m_parent->type()) {
                    case value_t::array: {
                        for (std::size_t i = 0; i < current->m_parent->m_value.array->size(); ++i) {
                            if (&current->m_parent->m_value.array->operator[](i) == current) {
                                tokens.emplace_back(std::to_string(i));
                                break;
                            }
                        }
                        break;
                    }

                    case value_t::object: {
                        for (const auto &element: *current->m_parent->m_value.object) {
                            if (&element.second == current) {
                                tokens.emplace_back(element.first.c_str());
                                break;
                            }
                        }
                        break;
                    }

                    case value_t::null:            // LCOV_EXCL_LINE
                    case value_t::string:          // LCOV_EXCL_LINE
                    case value_t::boolean:         // LCOV_EXCL_LINE
                    case value_t::number_integer:  // LCOV_EXCL_LINE
                    case value_t::number_unsigned: // LCOV_EXCL_LINE
                    case value_t::number_float:    // LCOV_EXCL_LINE
                    case value_t::binary:          // LCOV_EXCL_LINE
                    case value_t::discarded:       // LCOV_EXCL_LINE
                    default:                       // LCOV_EXCL_LINE
                        break;                     // LCOV_EXCL_LINE
                }
            }

            if (tokens.empty()) {
                return "";
            }

            auto str = std::accumulate(tokens.rbegin(), tokens.rend(), std::string{},
                                       [](const std::string &a, const std::string &b) {
                                           return concat(a, '/', detail::escape(b));
                                       });
            return concat('(', str, ") ");
#else
            static_cast<void>(leaf_element);
            return "";
#endif
        }

    private:
        /// an exception object as storage for error messages
        std::runtime_error m;
    };

    /// @brief exception indicating a parse error
    /// @sa https://json.nlohmann.me/api/basic_json/parse_error/
    class parse_error : public exception {
    public:
        /*!
    @brief create a parse error exception
    @param[in] id_       the id of the exception
    @param[in] pos       the position where the error occurred (or with
                         chars_read_total=0 if the position cannot be
                         determined)
    @param[in] what_arg  the explanatory string
    @return parse_error object
    */
        template<typename BasicJsonContext, enable_if_t<is_basic_json_context<BasicJsonContext>::value, int> = 0>
        static parse_error create(int id_, const position_t &pos, const std::string &what_arg, BasicJsonContext context) {
            std::string w = concat(exception::name("parse_error", id_), "parse error",
                                   position_string(pos), ": ", exception::diagnostics(context), what_arg);
            return {id_, pos.chars_read_total, w.c_str()};
        }

        template<typename BasicJsonContext, enable_if_t<is_basic_json_context<BasicJsonContext>::value, int> = 0>
        static parse_error create(int id_, std::size_t byte_, const std::string &what_arg, BasicJsonContext context) {
            std::string w = concat(exception::name("parse_error", id_), "parse error",
                                   (byte_ != 0 ? (concat(" at byte ", std::to_string(byte_))) : ""),
                                   ": ", exception::diagnostics(context), what_arg);
            return {id_, byte_, w.c_str()};
        }

        /*!
    @brief byte index of the parse error

    The byte index of the last read character in the input file.

    @note For an input with n bytes, 1 is the index of the first character and
          n+1 is the index of the terminating null byte or the end of file.
          This also holds true when reading a byte vector (CBOR or MessagePack).
    */
        const std::size_t byte;

    private:
        parse_error(int id_, std::size_t byte_, const char *what_arg)
            : exception(id_, what_arg), byte(byte_) {}

        static std::string position_string(const position_t &pos) {
            return concat(" at line ", std::to_string(pos.lines_read + 1),
                          ", column ", std::to_string(pos.chars_read_current_line));
        }
    };

    /// @brief exception indicating errors with iterators
    /// @sa https://json.nlohmann.me/api/basic_json/invalid_iterator/
    class invalid_iterator : public exception {
    public:
        template<typename BasicJsonContext, enable_if_t<is_basic_json_context<BasicJsonContext>::value, int> = 0>
        static invalid_iterator create(int id_, const std::string &what_arg, BasicJsonContext context) {
            std::string w = concat(exception::name("invalid_iterator", id_), exception::diagnostics(context), what_arg);
            return {id_, w.c_str()};
        }

    private:
        JSON_HEDLEY_NON_NULL(3)
        invalid_iterator(int id_, const char *what_arg)
            : exception(id_, what_arg) {}
    };

    /// @brief exception indicating executing a member function with a wrong type
    /// @sa https://json.nlohmann.me/api/basic_json/type_error/
    class type_error : public exception {
    public:
        template<typename BasicJsonContext, enable_if_t<is_basic_json_context<BasicJsonContext>::value, int> = 0>
        static type_error create(int id_, const std::string &what_arg, BasicJsonContext context) {
            std::string w = concat(exception::name("type_error", id_), exception::diagnostics(context), what_arg);
            return {id_, w.c_str()};
        }

    private:
        JSON_HEDLEY_NON_NULL(3)
        type_error(int id_, const char *what_arg) : exception(id_, what_arg) {}
    };

    /// @brief exception indicating access out of the defined range
    /// @sa https://json.nlohmann.me/api/basic_json/out_of_range/
    class out_of_range : public exception {
    public:
        template<typename BasicJsonContext, enable_if_t<is_basic_json_context<BasicJsonContext>::value, int> = 0>
        static out_of_range create(int id_, const std::string &what_arg, BasicJsonContext context) {
            std::string w = concat(exception::name("out_of_range", id_), exception::diagnostics(context), what_arg);
            return {id_, w.c_str()};
        }

    private:
        JSON_HEDLEY_NON_NULL(3)
        out_of_range(int id_, const char *what_arg) : exception(id_, what_arg) {}
    };

    /// @brief exception indicating other library errors
    /// @sa https://json.nlohmann.me/api/basic_json/other_error/
    class other_error : public exception {
    public:
        template<typename BasicJsonContext, enable_if_t<is_basic_json_context<BasicJsonContext>::value, int> = 0>
        static other_error create(int id_, const std::string &what_arg, BasicJsonContext context) {
            std::string w = concat(exception::name("other_error", id_), exception::diagnostics(context), what_arg);
            return {id_, w.c_str()};
        }

    private:
        JSON_HEDLEY_NON_NULL(3)
        other_error(int id_, const char *what_arg) : exception(id_, what_arg) {}
    };

} // namespace detail
NLOHMANN_JSON_NAMESPACE_END
